홈서버로 CDN 만들기
목차
개요
이번에 OSSCA 의 fedify
프로젝트에 참여하게 됐는데, 해당 라이브러리를 사용하는 액티비티 펍 인스턴스를 만들려다 보니 CDN이 필요했다.
처음에는 그냥 파일로 저장할까 했다가 이참에 남는 컴퓨터로 공부할 겸 CDN을 만들어보자고 생각했다.
NGINX
단순 파일 미러를 위해 NGINX를 사용했다.
우분투에 설치했기 때문에 apt
명령어로 설치했다.
sudo apt install nginx
sudo systemctl enable nginx
sudo systemctl start nginx
캐시용 디렉터리 /var/cache/nginx
를 만들고 권한을 설정한다.
sudo mkdir -p /var/cache/nginx
sudo chown www-data:www-data /var/cache/nginx
캐시용 NGINX 설정 파일을 /etc/nginx/sites-available/cdn.conf
에 작성했다.
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=cdn_cache:10m inactive=60m max_size=5g;
server {
listen <포트 번호>;
server_name <서버 도메인>;
# /media/* 요청 처리
location /media/ {
alias <미디어 파일 경로>;
try_files $uri =404;
add_header Cache-Control "public, max-age=31536000, immutable";
add_header X-CDN-Status "Static Alias";
}
}
<서버 도메인>
를 거쳐 <포트 번호>
로 들어오는 /media/*
요청은 <미디어 파일 경로>
에 있는 파일을 반환하고,
try_files $uri =404;
는 요청한 파일이 없으면 404 에러를 반환한다.
add_header
지시어는 응답 헤더에 캐시 관련 정보를 추가한다.
Cache-Control
헤더는 브라우저 캐시를 설정하고, X-CDN-Status
헤더는 CDN 상태를 나타낸다.
홈서버에 도메인 설정은 Cloudflare로 로컬서버 호스팅하기 참고.
설정 파일을 활성화하려면 /etc/nginx/sites-enabled/
디렉터리에 심볼릭 링크를 생성한다.
sudo ln -s /etc/nginx/sites-available/cdn.conf /etc/nginx/sites-enabled/
NGINX 설정을 테스트하고, 문제가 없으면 재시작한다. 방화벽도 설정해주자.
sudo nginx -t
sudo systemctl restart nginx
sudo ufw allow 'Nginx Full'
만약 주소로 접속은 했는데 NGINX 에러가 발생한다면 sudo tail -f /var/log/nginx/error.log
명령어로 에러 로그를 확인해보자.
나는 stat() "<파일 경로>" failed (13: Permission denied)
라는 에러가 났었다.
알고 보니 <미디어 파일 경로>
경로 뿐만이 아니라 각 부모 경로,
예를 들어 /home/user/media/
라면 /home
, /home/user
, /home/user/media
모두 NGINX가 읽을 수 있어야 했던 것이다.
만약 나와 비슷한 오류가 났다면 각 경로 별로 권한을 확인하고, NGINX가 읽을 수 있도록 권한을 설정해주자.
sudo chmod o+x /home
sudo chmod o+x /home/ubuntu
...
sudo chmod o+x /home/ubuntu/img
업로드 구현하기
CDN에 파일을 업로드하는 기능은 NGINX로는 구현할 수 없으므로, Deno를 사용해 간단한 웹 서버를 만들었다. 잘 올라가는 지 확인만 하는 용도이므로, 그냥 LLM 을 사용해 간단한 파일 업로드 서버를 만들었다.
import { unescape } from "@std/html/entities";
const SERVER_DOMAIN = Deno.env.get("SERVER_DOMAIN")!;
const SAVE_PATH = Deno.env.get("SAVE_PATH")!;
if (!SERVER_DOMAIN || !SAVE_PATH) {
throw new Error("SERVER_DOMAIN and SAVE_PATH environment variables must be set.");
}
if (!SAVE_PATH.endsWith("/")) {
throw new Error("SAVE_PATH must end with a '/'");
}
async function rootHandler(req: Request) {
const method: string = req.method;
if (method === "POST") {
const formData: FormData = await req.formData();
const file: File | null = formData?.get("file") as File;
if (!file) {
return new Response("File required but not provided.", { status: 400 });
}
const ext = file.name.split(".").pop()?.toLowerCase();
const fileName: string = crypto.randomUUID().replaceAll("-", "") + "." +
ext;
const filePath = `${SAVE_PATH}${fileName}`;
await Deno.mkdir(SAVE_PATH, { recursive: true });
await Deno.writeFile(filePath, new Uint8Array(await file.arrayBuffer()));
// 업로드된 파일로 리다이렉트
return Response.redirect(`${SERVER_DOMAIN}/media/data/${fileName}`, 303);
}
const inputForm = `
<form method="POST" enctype="multipart/form-data">
<input name="file" type="file" />
<button type="submit">Upload</button>
</form>
`;
return new Response(
unescape(inputForm),
{
headers: {
"Content-Type": "text/html; charset=utf-8",
},
},
);
}
Deno.serve({port:9000},rootHandler);
~~생각보다 더 대충인듯 inputForm
봐 개끔직~~
파일 올라가는 것만 확인하면 바로 CDN 서버로 리다이렉트한다.
다만 Deno 는 보안을 위해 외부 파일 시스템에 대한 접근을 제한하는 듯했다.
그래서 미디어 파일 저장 경로에 업로드 경로 심볼릭 링크로 연결해주었다.
sudo ln -s <미디어 파일 경로> <업로드 경로>
Tailscale 을 사용해 개인 도메인으로 연결해서 인증은 구현하지 않았다.
확인
잘 되는 듯 하다.